package top.codedance.iotp.client.plugins.sync.bacnet;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import top.codedance.iotp.client.ClientApplication;
import top.codedance.iotp.client.net.listenner.PluginService;
import top.codedance.iotp.client.plugins.sync.bacnet.util.JsscSerialPortInputStream;
import top.codedance.iotp.client.plugins.sync.bacnet.util.JsscSerialPortOutputStream;
import top.codedance.iotp.common.entity.PluginResultEntity;
import com.fazecast.jSerialComm.SerialPort;
import com.serotonin.bacnet4j.LocalDevice;
import com.serotonin.bacnet4j.RemoteDevice;
import com.serotonin.bacnet4j.exception.BACnetException;
import com.serotonin.bacnet4j.npdu.Network;
import com.serotonin.bacnet4j.npdu.mstp.Constants;
import com.serotonin.bacnet4j.npdu.mstp.MasterNode;
import com.serotonin.bacnet4j.npdu.mstp.MstpNetwork;
import com.serotonin.bacnet4j.transport.DefaultTransport;
import com.serotonin.bacnet4j.transport.Transport;
import com.serotonin.bacnet4j.type.Encodable;
import com.serotonin.bacnet4j.type.EncodedValue;
import com.serotonin.bacnet4j.type.enumerated.ObjectType;
import com.serotonin.bacnet4j.type.enumerated.PropertyIdentifier;
import com.serotonin.bacnet4j.type.primitive.Boolean;
import com.serotonin.bacnet4j.type.primitive.ObjectIdentifier;
import com.serotonin.bacnet4j.type.primitive.Real;
import com.serotonin.bacnet4j.type.primitive.SignedInteger;
import com.serotonin.bacnet4j.util.PropertyReferences;
import com.serotonin.bacnet4j.util.PropertyValues;
import com.serotonin.bacnet4j.util.ReadListener;
import com.serotonin.bacnet4j.util.RequestUtils;
import org.apache.log4j.Logger;
import org.ini4j.Ini;
import org.ini4j.Wini;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class BacNetMsTpService implements PluginService, Runnable {
    private static Logger logger = ClientApplication.getLogger(BacNetMsTpService.class);
    private static Map<Integer, LocalDevice> localDeviceMap = new HashMap<>();
    private static BacNetMsTpService self = null;
    private static int LoadDeviceId = 2001237;
    private Integer clientId;
    private Wini ini;
    private volatile CountDownLatch cdl = new CountDownLatch(1);

    private BacNetMsTpService(Integer clientId, Wini ini) {
        this.clientId = clientId;
        this.ini = ini;
    }

    public static PluginService getInstance(Integer clientId, Wini ini) {
        synchronized (BacNetMsTpService.class) {
            if(self == null) {
                synchronized (BacNetMsTpService.class) {
                    BacNetMsTpService bacNetMsTpService = new BacNetMsTpService(clientId, ini);
                    BacNetExecuterImpl.pluginServiceWorkerPool.submit(bacNetMsTpService);
                    self = bacNetMsTpService;
                }
            }
        }
        return self;
    }

    @Override
    public void run() {
        init();
        logger.debug("BacNetMsTp is Running.");
    }

    @Override
    public void init() {
        try {
            int baudRate = 9600, dataBits = 8, stopBits = SerialPort.ONE_STOP_BIT, parity = SerialPort.NO_PARITY, comClientId = 0;
            String comName = null;
            if (ini != null && ini.containsKey("bacnet")) {
                Ini.Section section = ini.get("bacnet");
                if (section.containsKey("com." + clientId)) {
                    comClientId = Integer.valueOf(section.get("com." + clientId));
                    Ini.Section sectionCom = ini.get("com");
                    if (sectionCom.containsKey("comName." + comClientId)) {
                        comName = sectionCom.get("comName." + comClientId);
                    } else {
                        throw new Exception("parameter comName is empty.");
                    }
                    if (sectionCom.containsKey("baudRate." + comClientId)) {
                        baudRate = Integer.parseInt(sectionCom.get("baudRate." + comClientId));
                    } else {
                        throw new Exception("parameter baudRate is empty.");
                    }
                    if (sectionCom.containsKey("dataBits." + comClientId)) {
                        dataBits = Integer.parseInt(sectionCom.get("dataBits." + comClientId));
                    } else {
                        throw new Exception("parameter dataBits is empty.");
                    }
                    if (sectionCom.containsKey("stopBits." + comClientId)) {
                        stopBits = Integer.parseInt(sectionCom.get("stopBits." + comClientId));
                    } else {
                        throw new Exception("parameter stopBits is empty.");
                    }
                    if (sectionCom.containsKey("parity." + comClientId)) {
                        parity = Integer.parseInt(sectionCom.get("parity." + comClientId));
                    } else {
                        throw new Exception("parameter parity is empty.");
                    }
                } else {
                    throw new Exception("parameter com is empty.");
                }
            } else {
                throw new Exception("bacnet_service parameters is empty.");
            }

            final jssc.SerialPort serialPort = new jssc.SerialPort(comName);
            serialPort.openPort();
            serialPort.setParams(baudRate, dataBits, stopBits, parity);
            try {
                JsscSerialPortInputStream in = new JsscSerialPortInputStream(serialPort);
                JsscSerialPortOutputStream out = new JsscSerialPortOutputStream(serialPort);
                MasterNode masterNode = new MasterNode("test", in, out, (byte) 0, 1);
                masterNode.setMaxInfoFrames(Constants.MAX_INFO_FRAMES);
                masterNode.setMaxMaster(Constants.MAX_MASTER);
                masterNode.setUsageTimeout(Constants.USAGE_TIMEOUT);
                Network network = new MstpNetwork(masterNode, 0);
                Transport transport = new DefaultTransport(network);
                transport.setTimeout(Transport.DEFAULT_TIMEOUT);
                transport.setSegTimeout(Transport.DEFAULT_SEG_TIMEOUT);
                transport.setSegWindow(Transport.DEFAULT_SEG_WINDOW);
                transport.setRetries(Transport.DEFAULT_RETRIES);
                //创建虚拟的本地设备，随意设置一个deviceId 设备的唯一标识
                LocalDevice localDevice = new LocalDevice(LoadDeviceId++, transport);
                //初始化本地设备
                localDevice.initialize();
                //搜寻网段内远程设备
                localDevice.startRemoteDeviceDiscovery();
                localDeviceMap.put(this.clientId, localDevice);
            } catch (Exception e){
                logger.info("Com port connect fail.");
                logger.debug(e.getMessage());
            } finally {
                serialPort.closePort();
            }
        } catch (Exception e){
            logger.info("BACNet local device initialize fail.");
            logger.debug(e.getMessage());
        } finally {
            cdl.countDown();
        }
    }

    private void waiting(){
        try {
            cdl.await(3, TimeUnit.SECONDS);
        }catch (Exception e){}
    }

    @Override
    public PluginResultEntity execute(JSONArray items) {
        waiting();
        PluginResultEntity pluginResultEntity = new PluginResultEntity();
        try {
            JSONArray readsArray = new JSONArray();
            JSONArray writesArray = new JSONArray();
            JSONObject joTmp;
            for (int i = 0;i < items.size();i++){
                joTmp = items.getJSONObject(i);
                if(joTmp.containsKey("isWrite") && joTmp.getBoolean("isWrite")){
                    writesArray.add(joTmp);
                }else{
                    readsArray.add(joTmp);
                }
            }
            JSONObject results = new JSONObject();
            if(readsArray.size() > 0) {
                reads(results, readsArray);
            }
            if(writesArray.size() > 0){
                writes(results, writesArray);
            }
            pluginResultEntity.setContext(results);
            pluginResultEntity.setOk(true);
        } catch (Exception e){
            pluginResultEntity.setOk(false);
            pluginResultEntity.setMsg(e.getMessage());
            logger.debug(e.getMessage());
        }
        return pluginResultEntity;
    }

    private void reads(JSONObject results, JSONArray items){
        LocalDevice localDevice = localDeviceMap.get(this.clientId);
        Map<Integer, JSONArray> groupItems = new HashMap<>();
        JSONObject joTmp;
        JSONArray jaTmp;
        for (int i = 0;i < items.size();i++){
            joTmp = items.getJSONObject(i);
            if(groupItems.containsKey(joTmp.getInteger("deviceId"))){
                jaTmp = groupItems.get(joTmp.getInteger("deviceId"));
                jaTmp.add(joTmp);
            }else{
                jaTmp = new JSONArray();
                jaTmp.add(joTmp);
                groupItems.put(joTmp.getInteger("deviceId"), jaTmp);
            }
        }
        Set<Integer> deviceIds = groupItems.keySet();
        for (Integer deviceId : deviceIds) {
            try {
                //获取某个远程设备
                RemoteDevice remoteDevice = localDevice.getRemoteDeviceBlocking(deviceId);
                //获取远程设备的标识符对象
                List<ObjectIdentifier> objectList = RequestUtils.getObjectList(localDevice, remoteDevice).getValues();
                JSONArray currentItems = groupItems.get(deviceId);
                for (int i = 0; i < currentItems.size(); i++) {
                    try {
                        final JSONObject currentJo = currentItems.getJSONObject(i);
                        List<ObjectIdentifier> filter = objectList.stream().filter(e -> e.getObjectType().equals(ObjectType.forId(currentJo.getIntValue("datatype"))) && e.getInstanceNumber() == currentJo.getIntValue("instance")).collect(Collectors.toList());
                        PropertyValues values = readValueByProperty(localDevice, remoteDevice, filter, null, PropertyIdentifier.forId(currentJo.getIntValue("property")));
                        Encodable property = values.get(filter.get(0), PropertyIdentifier.forId(currentJo.getIntValue("property")));
                        results.put(currentJo.getString("tag"), property.toString());
                    }catch (Exception ex){
                        logger.debug(ex.getMessage());
                    }
                }
            }catch (Exception e){
                logger.debug(e.getMessage());
            }
        }
    }

    private void writes(JSONObject results, JSONArray items){
        LocalDevice localDevice = localDeviceMap.get(this.clientId);
        Map<Integer, JSONArray> groupItems = new HashMap<>();
        JSONObject joTmp;
        JSONArray jaTmp;
        for (int i = 0;i < items.size();i++){
            joTmp = items.getJSONObject(i);
            if(groupItems.containsKey(joTmp.getInteger("deviceId"))){
                jaTmp = groupItems.get(joTmp.getInteger("deviceId"));
                jaTmp.add(joTmp);
            }else{
                jaTmp = new JSONArray();
                jaTmp.add(joTmp);
                groupItems.put(joTmp.getInteger("deviceId"), jaTmp);
            }
        }
        Set<Integer> deviceIds = groupItems.keySet();
        for (Integer deviceId : deviceIds) {
            try {
                //获取某个远程设备
                RemoteDevice remoteDevice = localDevice.getRemoteDeviceBlocking(deviceId);
                JSONArray currentItems = groupItems.get(deviceId);
                for (int i = 0; i < currentItems.size(); i++) {
                    final JSONObject currentJo = currentItems.getJSONObject(i);
                    try {
                        Encodable value;
                        Object v = currentJo.get("value");
                        if(v instanceof java.lang.Boolean) {
                            value = currentJo.getBooleanValue("value") ? Boolean.TRUE : Boolean.FALSE;
                        }else if(v instanceof BigDecimal){
                            value = new Real(currentJo.getFloatValue("value"));
                        } else if(v instanceof Integer) {
                            value = new SignedInteger(currentJo.getIntValue("value"));
                        } else {
                            value = new EncodedValue(currentJo.getBytes("value"));
                        }
                        RequestUtils.writePresentValue(localDevice, remoteDevice, new ObjectIdentifier(currentJo.getIntValue("datatype"), currentJo.getIntValue("instance")), value);
                        results.put(currentJo.getString("tag"), currentJo.getString("value"));
                    }catch (Exception ex){
                        results.put(currentJo.getString("tag"), ex.getMessage());
                        logger.debug(ex.getMessage());
                    }
                }
            }catch (Exception e){
                logger.debug(e.getMessage());
            }
        }
    }

    private static PropertyValues readValueByProperty(final LocalDevice localDevice, final RemoteDevice d, final List<ObjectIdentifier> ois, final ReadListener callback, PropertyIdentifier propertyIdentifier) throws BACnetException {
        if (ois.size() == 0) {
            return new PropertyValues();
        }
        final PropertyReferences refs = new PropertyReferences();
        for (final ObjectIdentifier oid : ois) {
            refs.add(oid, propertyIdentifier);
        }
        return RequestUtils.readProperties(localDevice, d, refs, false, callback);
    }

    @Override
    public PluginResultEntity execute(JSONObject jo) {
        waiting();
        PluginResultEntity pluginResultEntity = new PluginResultEntity();
        LocalDevice localDevice = localDeviceMap.get(this.clientId);
        List<RemoteDevice> remoteDevices = localDevice.getRemoteDevices();
        JSONArray ja = new JSONArray();
        Set<Integer> integers = localDeviceMap.keySet();
        for (RemoteDevice rd : remoteDevices) {
            if(!integers.contains(rd.getInstanceNumber())) {
                JSONObject tjo = new JSONObject();
                tjo.put("deviceId", rd.getInstanceNumber());
                tjo.put("info", rd.toString());
                try {
                    RemoteDevice remoteDevice = localDevice.getRemoteDeviceBlocking(rd.getInstanceNumber());
                    List<ObjectIdentifier> objectList = RequestUtils.getObjectList(localDevice, remoteDevice).getValues();
                    JSONArray jaT = new JSONArray();
                    for (ObjectIdentifier o : objectList) {
                        jaT.add(o.toString());
                    }
                    tjo.put("properties", jaT);
                }catch (Exception e){ }
                ja.add(tjo);
            }
        }
        pluginResultEntity.setContext(ja);
        pluginResultEntity.setOk(true);
        return pluginResultEntity;
    }
}
